package com.lifang.framework.aop.aspectj;

import cn.hutool.core.lang.Assert;
import com.lifang.framework.aop.framework.ReflectiveMethodInvocation;
import com.lifang.framework.core.LocalVariableParameterNameDiscoverer;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

/**
 * aspectJ 通知实现基类
 * 仅 aspectJ 注解相关
 */
public abstract class AbstractAspectJAdvice implements MethodInterceptor, Advice {

    // 切入点class
    private final Class<?> declaringClass;

    // 切入点方法名称
    private final String methodName;

    // 切入点的方法的参数
    private final Class<?>[] parameterTypes;

    // 切入点方法
    private final Method aspectJAdviceMethod;

    // 切入点
    private final AspectJExpressionPointcut pointcut;

    // 切入点实例
    private Object aspectjInstance;

    // 切入点方法名称
    private String[] argumentNames;

    // joinPoint参数坐标
    private int joinPointArgumentIndex = -1;

    private String returningName;

    private String throwingName;

    private final LocalVariableParameterNameDiscoverer nameDiscoverer = new LocalVariableParameterNameDiscoverer();

    private Map<String, Integer> argumentBindings;

    public Class<?> getDeclaringClass() {
        return declaringClass;
    }

    public abstract int getOrder();

    public final AspectJExpressionPointcut getPointcut() {
        return this.pointcut;
    }

    public void setAspectjInstance(Object aspectjInstance) {
        this.aspectjInstance = aspectjInstance;
    }

    public void setReturningName(String returningName) {
        this.returningName = returningName;
    }

    public void setThrowingName(String throwingName) {
        this.throwingName = throwingName;
    }

    public AbstractAspectJAdvice(Method aspectJAdviceMethod, AspectJExpressionPointcut pointcut) {
        Assert.notNull(aspectJAdviceMethod, "aspectJ method not be null");
        this.declaringClass = aspectJAdviceMethod.getDeclaringClass();
        this.methodName = aspectJAdviceMethod.getName();
        this.parameterTypes = aspectJAdviceMethod.getParameterTypes();
        this.aspectJAdviceMethod = aspectJAdviceMethod;
        this.pointcut = pointcut;
    }

    public static JoinPoint currentJoinPoint(ReflectiveMethodInvocation invocation) {
        return new MethodInvocationProceedingJoinPoint(invocation);
    }

    public void calculateArgumentBindings() {
        if (this.parameterTypes == null || this.parameterTypes.length == 0) {
            return;
        }
        for (int i = 0; i < this.parameterTypes.length; i++) {
            Class<?> parameterType = this.parameterTypes[i];
            if (maybeBindJoinPoint(parameterType, i)) {
                continue;
            }
            if (argumentNames == null) {
                argumentNames = nameDiscoverer.getParameterNames(this.aspectJAdviceMethod);
            }

            Assert.notNull(argumentNames, "Advice method [" + this.methodName + "] " +
                    "requires " + i + " arguments to be bound by name, but the argument names were not specified and could not be discovered.");
            if (this.argumentBindings == null) {
                this.argumentBindings = new HashMap<>();
            }

            int numExpectedArgumentNames = this.aspectJAdviceMethod.getParameterCount();
            if (this.argumentNames.length != numExpectedArgumentNames) {
                throw new IllegalStateException("Expecting to find " + numExpectedArgumentNames +
                        " arguments to bind by name in advice, but actually found " + this.argumentNames.length + " arguments.");
            }

            this.argumentBindings.put(this.argumentNames[i], i);
            if (this.returningName != null) {
                if (!this.argumentBindings.containsKey(this.returningName)) {
                    throw new IllegalStateException("Returning argument name '" + this.returningName + "' was not bound in advice arguments");
                }
            }

            if (this.throwingName != null) {
                if (!this.argumentBindings.containsKey(this.throwingName)) {
                    throw new IllegalStateException("Throwing argument name '" + this.throwingName + "' was not bound in advice arguments");
                }
            }
        }
    }

    private boolean maybeBindJoinPoint(Class<?> candidateParameterType, int index) {
        boolean result = JoinPoint.class == candidateParameterType || ProceedingJoinPoint.class == candidateParameterType;
        if (result) {
            this.joinPointArgumentIndex = index;
        }
        return result;
    }

    protected void invokeAdviceMethod() throws Throwable {
        invokeAdviceMethod(argBinding(null, null, null));
    }

    protected void invokeAdviceMethod(Object returnValue) throws Throwable {
        invokeAdviceMethod(argBinding(null, returnValue, null));
    }

    protected void invokeAdviceMethod(Throwable throwable) throws Throwable {
        invokeAdviceMethod(argBinding(null, null, throwable));
    }

    protected Object invokeAdviceMethod(JoinPoint joinPoint) throws Throwable {
        return invokeAdviceMethod(argBinding(joinPoint, null, null));
    }

    private Object invokeAdviceMethod(Object[] args) throws Throwable {
        if (Modifier.isPrivate(aspectJAdviceMethod.getModifiers())) {
            aspectJAdviceMethod.setAccessible(Boolean.TRUE);
        }
        try {
            return aspectJAdviceMethod.invoke(aspectjInstance, args);
        } catch (IllegalArgumentException ex) {
            throw new IllegalArgumentException("Mismatch on arguments to advice method [" + this.aspectJAdviceMethod + "]; pointcut expression [" +
                    this.getPointcut().getPointcutExpression() + "]", ex);
        } catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
    }

    private Object[] argBinding(JoinPoint joinPoint, Object returnValue, Throwable throwable) {
        Object[] args = new Object[this.parameterTypes.length];
        if (args.length > 0) {
            if (this.joinPointArgumentIndex != -1 && joinPoint == null) {
                throw new IllegalArgumentException("only around advice support bind JoinPoint and ProceedingJoinPoint method parameters," +
                        "form class " + declaringClass.getName() + " method " + methodName);
            }

            int offset = 0;
            if (this.joinPointArgumentIndex != -1) {
                args[joinPointArgumentIndex] = joinPoint;
                offset++;
            }

            if (this.returningName != null) {
                Integer index = this.argumentBindings.get(this.returningName);
                if (index != null) {
                    Class<?> parameterType = this.parameterTypes[index];
                    if (returnValue != null && !parameterType.isAssignableFrom(returnValue.getClass())) {
                        throw new IllegalArgumentException("Type Mismatch when binding afterReturningAdvice parameters target type " + parameterType.getName() +
                                " source type " + returnValue.getClass().getName());
                    }
                    args[index] = returnValue;
                    offset++;
                }
            }

            if (this.throwingName != null) {
                Integer index = this.argumentBindings.get(this.throwingName);
                if (index != null) {
                    Class<?> parameterType = this.parameterTypes[index];
                    if (!parameterType.isAssignableFrom(throwable.getClass())) {
                        throw new IllegalArgumentException("Type Mismatch when binding throwingAdvice parameters, target type " + parameterType.getName() +
                                " source type " + throwable.getClass().getName());
                    }
                    args[index] = throwable;
                    offset++;
                }
            }

            if (offset != args.length) {
                throw new IllegalArgumentException("Parameter binding failed for class " + declaringClass.getName() + " method " + methodName);
            }
        }
        return args;
    }
}